﻿using System;
using System.IO;
using System.Runtime.InteropServices;
using PentaBiz.Interop;
using PentaBiz.Serialization;

namespace PentaBiz.Collections.DataCache
{
    internal static class SequentialDataCacheStructures
    {
        #region Nested type: Head

        /// <summary>
        /// Collection Head
        /// </summary>
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        internal struct Head
        {
            public Page LastPage;
            public int PageSizeLimit;
            public int Count;

            //todo: maybe add reserved bytes...
            //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
            //public byte[] Reserved;

            public void Persist(Stream stream)
            {
                stream.Seek(0, SeekOrigin.Begin);
                stream.WriteStruct(this);
            }

            public static Head LoadFrom(Stream stream)
            {
                stream.Seek(0, SeekOrigin.Begin);
                return stream.ReadStruct<Head>();
            }

            /// <summary>
            /// Initializes this instance.
            /// Resets the Count to 0 and creates new Page with default settings
            /// </summary>
            public void Initialize()
            {
                Count = 0;
                var page = new Page
                               {
                                   PageIndex = 0,
                                   PreviousPageOffset = -1,
                                   NextPageOffset = -1,
                                   PageOffset = StructConstants.HeadSize,
                                   NextFreeDataOffset = StructConstants.HeadSize + StructConstants.PageSize,
                                   DataOffsets = new long[PageSizeLimit],
                               };

                LastPage = page;
            }


            public void Add<T>(Stream stream, T item, SequentialDataCache<T> cache) where T : class
            {

                bool isnew = false;
                if (LastPage.DataCount >= PageSizeLimit) //check if need to extend 
                {
                    //create new page.
                    Page nextPage = LastPage.RollToNextPage(PageSizeLimit);

                    nextPage.Persist(stream);
                    LastPage.Persist(stream);

                    LastPage = nextPage;
                    isnew = true;

                }


                LastPage.AppendData(stream, item, cache);

                Count++;

                Persist(stream);
                LastPage.Persist(stream);

                if (isnew)
                    cache.pageLookup.Add(LastPage.PageOffset);

            }


            public void Clear(Stream stream)
            {
                Initialize();

                Persist(stream);
                LastPage.Persist(stream);
            }

            public Page OpenFirstPage(Stream stream)
            {
                stream.Seek(StructConstants.HeadSize, SeekOrigin.Begin);
                var page = stream.ReadStruct<Page>();

                return page;
            }


        }

        #endregion

        #region Nested type: Page

        /// <summary>
        /// Page structure
        /// Stores page and data offsets
        /// </summary>
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        internal struct Page
        {
            /// <summary>
            /// The page index
            /// </summary>
            public long PageIndex;

            /// <summary>
            /// The previous page offset
            /// </summary>
            public long PreviousPageOffset;

            /// <summary>
            /// The next page offset
            /// </summary>
            public long NextPageOffset;


            /// <summary>
            /// The page offset
            /// </summary>
            public long PageOffset;

            /// <summary>
            /// The next free data offset
            /// </summary>
            public long NextFreeDataOffset;

            /// <summary>
            /// The data count
            /// </summary>
            public int DataCount;

            /// <summary>
            /// The data offsets
            /// </summary>
            public long[] DataOffsets;

            /// <summary>
            /// Reads all valid data from the page.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="stream">The stream.</param>
            /// <returns></returns>
            public T[] ReadAllData<T>(Stream stream)
            {
                var data = new T[DataCount];
                for (int i = 0; i < DataCount; i++)
                {
                    long dataOffset = DataOffsets[i];
                    if (dataOffset > 0)
                    {
                        stream.Seek(dataOffset, SeekOrigin.Begin);
                        var d = (T)NetSerializer<T>.Deserialize(stream);
                        data.SetValue(d, i);
                    }
                }

                return data;
            }

            /// <summary>
            /// Appends the data to the page.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="stream">The stream.</param>
            /// <param name="item">The item.</param>
            /// <param name="cache">The cache.</param>
            /// <exception cref="System.InvalidOperationException">Cannot Append any more data because this page has been already rolled</exception>
            public void AppendData<T>(Stream stream, T item, SequentialDataCache<T> cache) where T : class
            {
                if (NextFreeDataOffset == -1)
                    throw new InvalidOperationException(
                        "Cannot Append any more data because this page has been already rolled");

                var nextFreeDataOffset = NextFreeDataOffset;
                stream.Seek(nextFreeDataOffset, SeekOrigin.Begin);
                DataOffsets[DataCount] = nextFreeDataOffset;


                NetSerializer<T>.Serialize(stream, item);

                cache.dataLookup.Add(nextFreeDataOffset);
                DataCount++;
                NextFreeDataOffset = stream.Position;


            }

            /// <summary>
            /// Persists the the page with specified stream.
            /// </summary>
            /// <param name="stream">The stream.</param>
            public void Persist(Stream stream)
            {
                stream.Seek(PageOffset, SeekOrigin.Begin);
                stream.WriteStruct(this);
            }

            public bool IsFirstPage()
            {
                return PreviousPageOffset == -1;
            }

            public bool IsLastPage()
            {
                return NextPageOffset == -1;
            }


            public Page RollToNextPage(int pageSizeLimit)
            {
                if (NextFreeDataOffset == -1)
                    throw new InvalidOperationException(
                        "Cannot roll to next page because this page has been already rolled");

                NextPageOffset = NextFreeDataOffset;
                NextFreeDataOffset = -1;

                //create next page
                var page = new Page
                               {
                                   PageIndex = PageIndex + 1,
                                   PreviousPageOffset = PageOffset,
                                   NextFreeDataOffset = NextPageOffset + StructConstants.PageSize,
                                   NextPageOffset = -1,
                                   PageOffset = NextPageOffset,
                                   DataOffsets = new long[pageSizeLimit],
                               };

                return page;
            }


            /// <summary>
            /// Reads the next page.
            /// </summary>
            /// <param name="reader">The reader.</param>
            /// <returns></returns>
            /// <exception cref="System.IndexOutOfRangeException">This page is the Last page in the collection</exception>
            public Page ReadNextPage(Stream reader)
            {
                if (NextPageOffset == -1)
                    throw new IndexOutOfRangeException("This page is the Last page in the collection");

                reader.Seek(NextPageOffset, SeekOrigin.Begin);
                return reader.ReadStruct<Page>();
            }


            /// <summary>
            /// Reads the single data at the specified <paramref name="pageDataIndex"/>.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="reader">The reader.</param>
            /// <param name="pageDataIndex">Index of the page data.</param>
            /// <returns></returns>
            public T ReadSingleDataAt<T>(Stream reader, int pageDataIndex)
            {
                reader.Seek(DataOffsets[pageDataIndex], SeekOrigin.Begin);
                var d = (T)NetSerializer<T>.Deserialize(reader);

                return d;
            }

            /// <summary>
            /// Reads the single data at the specified <paramref name="pageDataOffset" />.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="reader">The reader.</param>
            /// <param name="pageDataOffset">The page data offset.</param>
            /// <returns></returns>
            public static T ReadSingleDataAt<T>(Stream reader, long pageDataOffset)
            {
                reader.Seek(pageDataOffset, SeekOrigin.Begin);
                var d = (T)NetSerializer<T>.Deserialize(reader);

                return d;
            }
        }

        #endregion

        #region Nested type: StructConstants

        internal static class StructConstants
        {
            /// <summary>
            /// The head unmanaged size
            /// </summary>
            public static readonly int HeadSize = Marshal.SizeOf(typeof(Head));

            /// <summary>
            /// The page unmanaged size
            /// </summary>
            public static readonly int PageSize = Marshal.SizeOf(typeof(Page));
        }

        #endregion
    }
}